package edu.zhuravlev;

import edu.zhuravlev.exception.TrainingParseException;
import edu.zhuravlev.model.Exercise;
import edu.zhuravlev.model.TrainingResults;
import lombok.SneakyThrows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

class TrainingMessageParserTest {

    private static final Method PARSE_ROW_METHOD;

    private static final Method PARSE_WEIGHT_AND_REPEATS_METHOD;

    static {
        try {
            PARSE_ROW_METHOD = TrainingMessageParser.class.getDeclaredMethod("parseRow", String.class);
            PARSE_WEIGHT_AND_REPEATS_METHOD = TrainingMessageParser.class.getDeclaredMethod("parseWeightAndRepeatsPair", String.class, String.class);

            PARSE_ROW_METHOD.setAccessible(true);
            PARSE_WEIGHT_AND_REPEATS_METHOD.setAccessible(true);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    private final TrainingMessageParser parser = new TrainingMessageParser();

    @Test
    @DisplayName("Общий тест парсера")
    @SneakyThrows
    void parse_test() {
        List<Exercise> exercises = new ArrayList<>();

        exercises.add(Exercise.builder().exerciseName("битцуха").weightAndRepeats(Map.of(15d, List.of(12,10,8,6))).build());
        exercises.add(Exercise.builder().exerciseName("плечи").weightAndRepeats(Map.of(10d, List.of(14,10), 8d, List.of(8, 6))).build());
        exercises.add(Exercise.builder().exerciseName("жим лежа").weightAndRepeats(Map.of(30d, List.of(14,10), 25.5, List.of(8))).build());

        TrainingResults testResult = new TrainingResults(exercises);
        String dataRepresentation = """
                                    Битцуха: (15кг): 12,10,8,6
                                    Плечи: (10кг): 14,10: (8кг): 8,6
                                    Жим лежа: (30кг): 14,10: (25.5кг): 8
                                    """;

        TrainingResults parseResults = parser.parse(dataRepresentation);

        assertEquals(testResult, parseResults);
    }

    @Test
    @DisplayName("Попытка обработки пустых данных или null")
    void parse_null_or_empty_data_throw_error() {
        assertThrows(TrainingParseException.class, () -> parser.parse(null));
        assertThrows(TrainingParseException.class, () -> parser.parse(""));
    }

    @Test
    @DisplayName("Тест парсинга одной строки")
    @SneakyThrows
    void parse_row_test() {
        String row = "Плечи: (10кг): 14,10: (8кг): 8,6";

        Exercise expected = Exercise.builder().exerciseName("плечи").weightAndRepeats(Map.of(10d, List.of(14,10), 8d, List.of(8, 6))).build();
        Exercise actual = callReflectParseRow(row);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName("Тест обработки некорретных данных в строках")
    @SneakyThrows
    void parse_incorrect_row_produce_exceptions() {
        String incorrectRowFormat = "Плечи: (10кг): 14,10 (8кг): 8,6";

        TrainingParseException incorrectFormatException = assertThrows(TrainingParseException.class, () -> callReflectParseRow(incorrectRowFormat));
        assertEquals("Передана строка неправильного формата: [%s]".formatted(incorrectRowFormat), incorrectFormatException.getMessage());

        String incorrectExerciseName = " : (10кг): 14,10: (8кг): 8,6";

        TrainingParseException incorrectExerciseNameException = assertThrows(TrainingParseException.class, () -> callReflectParseRow(incorrectExerciseName));
        assertEquals("Название упражнения не может быть пустым. Ошибка в строке: [%s]".formatted(incorrectExerciseName), incorrectExerciseNameException.getMessage());
    }

    @Test
    @DisplayName("Тест парсинга веса и повторений")
    @SneakyThrows
    void parse_weight_and_repeats() {
        String weight = "(15кг)";
        String repeats = "14,12,8,7";

        Map.Entry<Double, List<Integer>> expected = new AbstractMap.SimpleEntry<>(15d, List.of(14, 12, 8, 7));
        Map.Entry<Double, List<Integer>> actual = callReflectParseWeightAndRepeatsPair(weight, repeats);

        assertEquals(expected, actual);
    }

    @ParameterizedTest
    @MethodSource("incorrectWeightParams")
    @DisplayName("Тест обработки некорретного значения веса")
    @SneakyThrows
    void parse_incorrect_weight_produce_exception(String incorrectWeight) {
        String repeats = "10,9,8,7";

        TrainingParseException incorrectWeightException = assertThrows(TrainingParseException.class, () -> callReflectParseWeightAndRepeatsPair(incorrectWeight, repeats));
        assertEquals("Указанный вес не соответствует шаблону: [%s]".formatted(incorrectWeight), incorrectWeightException.getMessage());
    }

    @ParameterizedTest
    @MethodSource("incorrectRepeatsParams")
    @DisplayName("Тест обработки некорретного значения повторений")
    @SneakyThrows
    void parse_incorrect_repeats_produce_exception(String incorrectRepeats) {
        String weight = "(15кг)";

        TrainingParseException incorrectRepeatsException = assertThrows(TrainingParseException.class, () -> callReflectParseWeightAndRepeatsPair(weight, incorrectRepeats));
        assertEquals("Число повторений не соответствует шаблону: [%s]".formatted(incorrectRepeats.trim()), incorrectRepeatsException.getMessage());
    }

    private static Stream<Arguments> incorrectRepeatsParams() {
        return Stream.of(
                Arguments.of("14,10,9, "),
                Arguments.of("15, 14, 12.3"),
                Arguments.of(",124,5,3"),
                Arguments.of("-10"),
                Arguments.of("14,12,-10,5")
        );
    }

    private static Stream<Arguments> incorrectWeightParams() {
        return Stream.of(
                Arguments.of("15кг"),
                Arguments.of("(43)"),
                Arguments.of("(-14кг)"),
                Arguments.of("(10кц)"),
                Arguments.of("(25,3кг)"),
                Arguments.of("12,5кг"),
                Arguments.of("(пывп)")
        );
    }

    private Exercise callReflectParseRow(String row) throws Throwable {
        Exercise result;

        try {
            result = (Exercise) PARSE_ROW_METHOD.invoke(parser, row);
        } catch (Exception ex) {
            Throwable cause = ex.getCause();

            if (cause instanceof TrainingParseException) {
                throw cause;
            }

            throw ex;
        }

        return result;
    }

    private Map.Entry<Double, List<Integer>> callReflectParseWeightAndRepeatsPair(String weight, String repeats) throws Throwable {
        Map.Entry<Double, List<Integer>> result;

        try {
            result = (Map.Entry<Double, List<Integer>>) PARSE_WEIGHT_AND_REPEATS_METHOD.invoke(parser, weight, repeats);
        } catch (Exception ex) {
            Throwable cause = ex.getCause();

            if (cause instanceof TrainingParseException) {
                throw cause;
            }

            throw ex;
        }

        return result;
    }
}